import tkinter as tk
import time
import logging, logg

# Useful third party website
# https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/frame.html
# https://tkdocs.com/tutorial/widgets.html 
# https://www.tutorialspoint.com/python3/python_gui_programming.htm 

# note format:
    # resource name: 
    # note:
    # resource value:

# current file logger
cf_logger = logging.getLogger('Tk_widgets_sample.basic_widgets')

class FrameCustom(object):
    def __init__(self, master=None):
        # get master
        self.master = master

        if self.master is None:
            self.master = tk.Tk()

        # init frame
        self.frm = tk.Frame(self.master)
        self.frm['width'] = 200
        self.frm['height'] = 100
        self.frm.pack()

    def resource_set(self, key, value):
        """
        Set the widget
        :param key: resource name
        :param value: value
        :return: None
        """
        # Setting method:
        # config -> self.frm.config(bg='red'), self.frm.config(background='red'),
        # configure -> self.frm.configure(bg='red'), self.frm.configure(background='red'),
        # self.frm['bg'] = 'red', self.frm['background'] = 'red'

        # Valid resource names:
        # background, bd, bg, borderwidth, class,
        # colormap, container, cursor, height, highlightbackground,
        # highlightcolor, highlightthickness, relief, takefocus, visual, width.

        self.frm[key] = value

    def resource_set_test(self):
        """
        Set the widget resource name with different value
        Details refer to below link:
        https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/frame.html

        Try to set colormap, container, class, visual, the error pops up - "all of them could not be 
        changed after the widget has been created."
        """
        # resource name: colormap
        # note: Few old screens support 256 or fewer colors. So the developer has to set limitations for colors. 
        # In that case, supported colors are mapped so that only those colors can be implemented in the entire 
        # application. Yes, different windows can inherit this color or can create a new one.
        # resource value:'new'
        self.resource_set('colormap', 'new')
        cf_logger.info('FrameCustom object set colormap.')


        # resource name: bg or background
        # resource value: red, yellow, blue, green ...
        # resource value: #FFFAFA, details in https://www.sioe.cn/yingyong/yanse-rgb-16/
        self.resource_set('bg', 'red')
        self.resource_set('background', '#FFFAFA')
        cf_logger.info('FrameCustom object set background.')

        # resource name: bd or borderwidth
        # resource value: 0, 1, 2.3 ...
        self.resource_set('bd', 2)
        self.resource_set('borderwidth', 3.5)
        cf_logger.info('FrameCustom object set borderwidth.')

        # resource name: width, height
        # resource value: 200, 300, ...
        self.resource_set('width', 500)
        self.resource_set('height', 300)
        cf_logger.info('FrameCustom object set width.')
        cf_logger.info('FrameCustom object set height.')

        # resource name: cursor
        # note: Shape when the mouse cursor is moved over the frame
        # resource value:  “arrow”,”circle”,”clock”,”cross”,”dotbox”,”exchange”,”fleur”,”heart”,”heart”,”man”,
        # resource value:  “mouse”,”pirate”,”plus”,”shuttle”,”sizing”,”spider”,”spraycan”,”star”,”target”,”cross”,
        # resource value:  “trek”,”watch”. https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/cursors.html 
        self.resource_set('cursor', 'cross')
        cf_logger.info('FrameCustom object set cursor.')

        # resource name: highlightbackground
        # note: Color of the focus highlight when the frame does not have focus.
        # resource value: red, yellow, blue, green ...
        # resource value: #FFFAFA, details in https://www.sioe.cn/yingyong/yanse-rgb-16/
        self.resource_set('highlightbackground', 'red')
        self.resource_set('highlightbackground', '#FFFAFA')
        cf_logger.info('FrameCustom object set highlightbackground.')

        # resource name: highlightcolor
        # note: Color shown in the focus highlight when the frame has the focus.
        # resource value: red, yellow, blue, green ...
        # resource value: #FFFAFA, details in https://www.sioe.cn/yingyong/yanse-rgb-16/
        self.resource_set('highlightcolor', 'green')
        self.resource_set('highlightcolor', '#FFDAB9')
        cf_logger.info('FrameCustom object set highlightcolor.')

        # resource name: highlightthickness
        # note: Thickness of the focus highlight.
        # resource value: 1, 2.5, 3...
        self.resource_set('highlightthickness', 5)
        cf_logger.info('FrameCustom object set highlightthickness.')

        # resource name: relief
        # note: The casing outside the style. Default is relief = FLAT
        # resource value: RAISED='raised', SUNKEN='sunken', FLAT='flat', RIDGE='ridge', GROOVE='groove', SOLID = 'solid'
        self.resource_set('relief', tk.SUNKEN)
        self.resource_set('relief', 'groove')
        cf_logger.info('FrameCustom object set relief.')

        # resource name: takefocus
        # note: Specifies whether the component accepts input focus (the user can switch it to by tabbing). Default is False.
        # note: Set this as True, then the highlightcolor could be worked.
        # resource value: True, False
        self.resource_set('takefocus', True)
        cf_logger.info('FrameCustom object set takefocus.')

        # run mainloop
        self.run()

    def run(self):
        # run master mainloop
        self.master.mainloop()


class LabelFrameCustom(object):
    def __init__(self, master=None):
        # get master
        self.master = master

        if self.master is None:
            self.master = tk.Tk()

        # init labelframe
        self.label_frm = tk.LabelFrame(self.master)
        self.label_frm['width'] = 200
        self.label_frm['height'] = 100
        self.label_frm.pack()

    def resource_set(self, key, value):
        """
        Set the widget
        :param key: resource name
        :param value: value
        :return: None
        """
        # Setting method:
        # config -> self.label_frm.config(bg='red'), self.label_frm.config(background='red'),
        # configure -> self.label_frm.configure(bg='red'), self.label_frm.configure(background='red'),
        # self.label_frm['bg'] = 'red', self.label_frm['background'] = 'red'

        # STANDARD OPTIONS

        #    borderwidth, cursor, foreground,
        #    highlightbackground, highlightcolor,
        #    highlightthickness, padx, pady, relief,
        #    takefocus, text

        # WIDGET-SPECIFIC OPTIONS

        #    background, class, colormap, container,
        #    height, labelanchor, labelwidget,
        #    visual, width

        self.label_frm[key] = value
    
    def resource_set_test(self):
        """
        Set the widget resource name with different value
        
        Only try foreground, padx, pady, textlabelanchor, labelwidget, text
        Others refer to FrameCustom class
        """
        # resource name: foreground
        # note: Color to be used for the label text.
        # resource value: red, yellow, blue, green ...
        # resource value: #FFFAFA, details in https://www.sioe.cn/yingyong/yanse-rgb-16/
        self.resource_set('fg', 'red')
        self.resource_set('foreground', '#FFFAFA')
        cf_logger.info('LabelFrameCustom object set foreground.')

        # resource name: padx, pady
        # resource value: 1,2...
        self.resource_set('padx', 5)
        self.resource_set('pady', 5)
        cf_logger.info('LabelFrameCustom object set padx.')
        cf_logger.info('LabelFrameCustom object set pady.')

        # resource name: textlabelanchor
        # note: Use this option to specify the position of the label on the widget's border. The default position is 'nw', 
        # which places the label at the left end of the top border. For the nine possible label positions, 
        # refer to this diagram:
        #  ---‘nw’---------------'n'------------'ne'----------
        # |'wn'                                          'en' |
        # |'w'                                           'e'  |
        # |'ws'                                          'es' |
        # |----'sw'--------------'s'------------'se'----------
        # resource value: 'n' 'w'...
        self.resource_set('textlabelanchor', 'n')
        cf_logger.info('LabelFrameCustom object set textlabelanchor.')

        # resource name: labelwidget
        # note:Instead of a text label, you can use any widget as the label by passing that widget as the value of this option. 
        # If you supply both labelwidget and text options, the text option is ignored.
        # resource value: widget
        label_widget = tk.Label(text='LabelFrame')
        self.resource_set('labelwidget', label_widget)
        cf_logger.info('LabelFrameCustom object set labelwidget.')

        # resource name: text
        # note:Text of the label.
        # resource value: string
        self.resource_set('text', 'LabelFrame title')
        cf_logger.info('LabelFrameCustom object set text.')

        # run mainloop
        self.run()

    def run(self):
        # run master mainloop
        self.master.mainloop()


class LabelCustom(object):
    def __init__(self, master=None):
        # get master
        self.master = master

        if self.master is None:
            self.master = tk.Tk()

        # init label
        self.label = tk.Label(self.master)
        self.label['width'] = 100
        self.label['height'] = 50
        self.label.pack()

    def resource_set(self, key, value):
        """
        Set the widget
        :param key: resource name
        :param value: value
        :return: None
        """
        # Setting method:
        # config -> self.label.config(bg='red'), self.label.config(background='red'),
        # configure -> self.label.configure(bg='red'), self.label.configure(background='red'),
        # self.label['bg'] = 'red', self.label['background'] = 'red'

        # STANDARD OPTIONS
        #    activebackground, activeforeground, anchor,
        #    background, bitmap, borderwidth, cursor,
        #    disabledforeground, font, foreground,
        #    highlightbackground, highlightcolor,
        #    highlightthickness, image, justify,
        #    padx, pady, relief, takefocus, text,
        #    textvariable, underline, wraplength

        # WIDGET-SPECIFIC OPTIONS

        #    height, state, width

        self.label[key] = value
    
    def resource_set_test(self):
        """
        Set the widget resource name with different value
        
        Only try activebackground, activeforeground, disabledforeground, state, text, anchor, font, textvariable, 
        justify, underline, wraplength, bitmap, image

        Others refer to FrameCustom class
        """
        # resource name: activebackground
        # note: when the state is active, the background color
        # resource value: red, yellow, blue, green ...
        # resource value: #FFFAFA, details in https://www.sioe.cn/yingyong/yanse-rgb-16/
        self.resource_set('activebackground', 'red')
        self.resource_set('activebackground', '#FFFAFA')
        cf_logger.info('LabelCustom object set activebackground.')

        # resource name: activeforeground
        # note: when the state is active, the foreground color
        # resource value: red, yellow, blue, green ...
        # resource value: #FFFAFA, details in https://www.sioe.cn/yingyong/yanse-rgb-16/
        self.resource_set('activeforeground', 'green')
        self.resource_set('activeforeground', '#FFFAFF')
        cf_logger.info('LabelCustom object set activeforeground.')

        # resource name: disabledforeground
        # note: when the state is disable, the foreground color
        # resource value: red, yellow, blue, green ...
        # resource value: #FFFAFA, details in https://www.sioe.cn/yingyong/yanse-rgb-16/
        self.resource_set('disabledforeground', 'yellow')
        self.resource_set('activeforeground', '#FFFAAA')
        cf_logger.info('LabelCustom object set disabledforeground.')

        # resource name: state
        # note: the state of the label
        # resource value: tk.NORMAL, tk.DISABLED, tk.ACTIVE
        self.resource_set('state', tk.ACTIVE)
        cf_logger.info('LabelCustom object set state.')

        # resource name: text
        # note:Text of the label.
        # resource value: string
        self.resource_set('text', 'Label text')
        cf_logger.info('LabelCustom object set text.')

        # resource name: anchor
        # note: Use this option to specify the position of the text
        #  ---‘nw’---------------'n'------------'ne'----------
        # |                                                   |
        # |'w'                   'center'                'e'  |
        # |                                                   |
        # |----'sw'--------------'s'------------'se'----------
        # resource value: 'n' 'w'...
        self.resource_set('anchor', 'n')
        cf_logger.info('LabelCustom object set anchor.')

        # resource name: font
        # note:Text font
        # resource value: Font object
        self.resource_set('font', ('Arial', 25))
        cf_logger.info('LabelCustom object set font.')

        # resource name: textvariable
        # note: text
        # resource value:  below objects
        # v = tk.DoubleVar()   # Holds a float; default value 0.0
        # v = tk.IntVar()      # Holds an int; default value 0
        # v = tk.StringVar()   # Holds a string; default value '' object
        # v.get(), v.set('text') to get and set the text
        var = tk.StringVar()
        self.resource_set('textvariable', var)
        cf_logger.info('LabelCustom object set textvariable.')

        # resource name: justify
        # note:Specifies how multiple lines of text will be aligned with respect to each other: tk.LEFT for flush left, 
        # tk.CENTER for centered (the default), or tk.RIGHT for right-justified.
        # resource value: tk.LEFT, tk.RIGHT, tk.CENTER
        self.resource_set('justify', tk.LEFT)
        cf_logger.info('LabelCustom object set justify.')
        
        # resource name: underline
        # note:underline nth letter of text, like 1 to underline the 1th letter, default is -1 means none
        # note:underline all leters , could set the font object underline configuration
        # resource value: 0, 1, 2...
        self.resource_set('underline', 0)
        cf_logger.info('LabelCustom object set underline.')

        # resource name: wraplength
        # note:Specify how many units to wrap the line. The default value, 0, means that lines will be broken only at newlines.
        # resource value: 0, 1, 2...
        self.resource_set('wraplength', 10)
        cf_logger.info('LabelCustom object set wraplength.')
        
        # resource name: bitmap, image
        # note:Set this option equal to a bitmap or image object and the label will display that graphic.
        # note: https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/bitmaps.html, https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/images.html#class-PhotoImage
        # The graphic above shows Button widgets bearing the standard bitmaps. 
        # You can use your own bitmaps. Any file in .xbm (X bit map) format will work. In place of a standard bitmap name, 
        # use the string '@' followed by the pathname of the .xbm file.
        # resource value: From left to right, they are 'error', 'gray75', 'gray50', 'gray25', 'gray12', 'hourglass', 'info', 'questhead', 'question', and 'warning'.

        self.resource_set('bitmap', 'error')  # use own bitmaps, refer to logo = tk.BitmapImage('logo.xbm', foreground='red')
        cf_logger.info('LabelCustom object set bitmap.')

        image_object = tk.PhotoImage(file='') # should set the image file path as f var first
        self.resource_set('image', image_object)
        cf_logger.info('LabelCustom object set image.')
        
        # run mainloop
        self.run()
    
    def run(self):
        # run master mainloop
        self.master.mainloop()


class EntryCustom(object):
    def __init__(self, master=None):
        # get master
        self.master = master

        if self.master is None:
            self.master = tk.Tk()

        # init entry
        self.entry = tk.Entry(self.master)
        self.entry['width'] = 100
        self.entry['height'] = 100
        self.entry.pack()
    
    def resource_set(self, key, value):
        """
        Set the widget
        :param key: resource name
        :param value: value
        :return: None
        """
        # Setting method:
        # config -> self.entry.config(bg='red'), self.entry.config(background='red'),
        # configure -> self.entry.configure(bg='red'), self.entry.configure(background='red'),
        # self.entry['bg'] = 'red', self.entry['background'] = 'red'

        # Valid resource names: 
        # background, bd, bg, borderwidth, cursor,
        # exportselection, fg, font, foreground, highlightbackground,
        # highlightcolor, highlightthickness, insertbackground,
        # insertborderwidth, insertofftime, insertontime, insertwidth,
        # invalidcommand, invcmd, justify, relief, selectbackground,
        # selectborderwidth, selectforeground, show, state, takefocus,
        # textvariable, validate, validatecommand, vcmd, width,
        # xscrollcommand.

        self.entry[key] = value

    def resource_set_test(self):
        """
        Set the widget resource name with different value
        
        Only try state, show, exportselection, insertbackground, insertborderwidth, insertwidth, insertofftime, insertontime, 
        selectbackground, selectborderwidth, selectforeground, readonlybackground, validate, validatecommand, invalidcommand, 
        invcmd, vcmd, xscrollcommand, 
        
        Others refer to FrameCustom or other above class
        """
        # resource name: state
        # note: state of Entry object
        # resource value: tk.NORMAL, tk.DISABLED, 'readonly', 'disabled'
        self.resource_set('state', tk.NORMAL)
        cf_logger.info('EntryCustom object set state.')

        # resource name: show
        # note: Normally, the characters that the user types appear in the entry. To make a “password” entry 
        # that echoes each character as an asterisk, set show='*'.
        # resource value: string
        self.resource_set('show', '*')
        cf_logger.info('EntryCustom object set show.')

        # resource name: exportselection
        # note: By default, if you select text within an Entry widget, it is automatically exported to the clipboard. 
        # To avoid this exportation, use exportselection=0.
        # resource value: 0
        self.resource_set('exportselection', 0)
        cf_logger.info('EntryCustom object set exportselection.')

        # resource name: insertbackground
        # note: insert cursor corlor
        # resource value: red, yellow, blue, green ...
        # resource value: #FFFAFA, details in https://www.sioe.cn/yingyong/yanse-rgb-16/
        self.resource_set('insertbackground', 'red')
        self.resource_set('insertbackground', '#FFFAFA')
        cf_logger.info('LabelCustom object set insertbackground.')

        # resource name: insertborderwidth
        # note: insert cursor borderwidth
        # resource value: 0, 1, 2...
        self.resource_set('insertborderwidth', 0)
        cf_logger.info('EntryCustom object set insertborderwidth.')

        # resource name: insertwidth
        # note: insert cursor width
        # resource value: 0, 1, 2...
        self.resource_set('insertwidth', 20)
        cf_logger.info('EntryCustom object set insertwidth.')

        # resource name: insertofftime
        # note: By default, the insertion cursor blinks. You can set insertofftime to a value in milliseconds 
        # to specify how much time the insertion cursor spends off. Default is 300. If you use insertofftime=0, 
        # the insertion cursor won't blink at all.
        # resource value: int
        self.resource_set('insertofftime', 500)
        cf_logger.info('EntryCustom object set insertofftime.')

        # resource name: insertontime
        # note: Similar to insertofftime, this option specifies how much time the cursor spends on per blink. 
        # Default is 600 (milliseconds).
        # resource value: int
        self.resource_set('insertontime', 800)
        cf_logger.info('EntryCustom object set insertontime.')

        # resource name: selectbackground
        # note: The background color to use displaying selected text.
        # resource value: red, yellow, blue, green ...
        # resource value: #FFFAFA, details in https://www.sioe.cn/yingyong/yanse-rgb-16/
        self.resource_set('selectbackground', 'green')
        self.resource_set('selectbackground', '#FFFAFF')
        cf_logger.info('LabelCustom object set selectbackground.')

        # resource name: selectforeground
        # note: The foreground (text) color of selected text.
        # resource value: red, yellow, blue, green ...
        # resource value: #FFFAFA, details in https://www.sioe.cn/yingyong/yanse-rgb-16/
        self.resource_set('selectforeground', 'yellow')
        self.resource_set('selectforeground', '#FFAAFF')
        cf_logger.info('LabelCustom object set selectforeground.')

        # resource name: selectborderwidth
        # note: The width of the border to use around selected text. The default is one pixel.
        # resource value: int
        self.resource_set('selectborderwidth', 50)
        cf_logger.info('LabelCustom object set selectborderwidth.')

        # resource name: readonlybackground
        # note: The background color to be displayed when the widget's state option is 'readonly'.
        # resource value: red, yellow, blue, green ...
        # resource value: #FFFAFA, details in https://www.sioe.cn/yingyong/yanse-rgb-16/
        self.resource_set('readonlybackground', 'blue')
        self.resource_set('readonlybackground', '#FFAFFF')
        cf_logger.info('LabelCustom object set readonlybackground.')

        # resource name: validate, validatecommand, invalidcommand, invcmd, vcmd
        # note:You can use this option to set up the widget so that its contents are checked by a validation function at certain times. 
        # details refer to https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/entry-validation.html
        # resource value: validate - 'focus', 'focusin', 'focusout', 'key', 'all', 'none'
        self.resource_set('validate', 'key')
        cf_logger.info('EntryCustom object set validate.')

        okCommand = self.entry.register(self._isOk)
        notOkCommand = self.entry.register(self._isNotOk)
        self.resource_set('validatecommand', (okCommand, '%s', '%S'))  # validatecommand, vcmd are the same
        self.resource_set('invalidcommand', (notOkCommand, '%s')) # invalidcommand, invcmd are the same
        cf_logger.info('EntryCustom object set validatecommand.') 
        cf_logger.info('EntryCustom object set validatecommand.')

        # resource name: textvariable
        # note: text
        # resource value:  below objects
        # v = tk.DoubleVar()   # Holds a float; default value 0.0
        # v = tk.IntVar()      # Holds an int; default value 0
        # v = tk.StringVar()   # Holds a string; default value '' object
        # v.get(), v.set('text') to get and set the text
        self.var = tk.StringVar()
        self.resource_set('textvariable', self.var)
        cf_logger.info('EntryCustom object set textvariable.')

        # resource name: xscrollcommand
        # note: combine the entry to a scroll bar
        # resource value: scroll object set 
        # details refer to https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/entry-scrolling.html

        # run mainloop
        self.run()
    
    def methods_entry_test(self):
        """
        Try all common methods of Entry object.
        Not really run below methods, just try to understand it by doc.
        """
        # get text of entry
        text = self.entry.get()
        cf_logger.info('entry content: {}'.format(text))

        # get cursor position
        cursor_position = self.entry.index(5)
        cf_logger.info('cursor_position: {}'.format(cursor_position))

        # delete first - last characther
        self.entry.delete(0, 2)

        # insert cursor position
        self.entry.icursor(5)

        # insert string at index
        self.entry.insert(5, 'insert string')

        # move text content by mouse
        # bind mouse button down event to entry, and call scan_mark(x), x is the mouse current position
        # bind mouse motion event to entry, and call scan_dragto(x), x is the mouse current position
        self.entry.scan_mark(5) #simulate the mouse moving
        time.sleep(1)
        self.entry.scan_dragto(10)

        # expand the selection to the index, if content text not enough to 20, cursor will expand to 20
        self.entry.selection_adjust(20)

        # cancel selection
        self.entry.selection_clear()

        # set the fixed end of a selection to INDEX
        self.entry.selection_from(10)

        # return True if there are characters selected in the entry, False otherwise
        self.entry.selection_present()

        # set selection from start to end (not included), end value could be tk.END
        self.entry.selection_range(0, 10)

        # selects all the text from the from position up to but not including the character at the given index
        self.entry.selection_to(20)

        # Positions the text in the entry so that the character at position f, relative to the entire text, 
        # is positioned at the left edge of the window. The f argument must be in the range [0,1], 
        # where 0 means the left end of the text and 1 the right end.
        self.entry.xview_moveto(0.5)

        # Used to scroll the entry horizontally. The what argument must be either tk.UNITS, to scroll by character widths, 
        # or tk.PAGES, to scroll by chunks the size of the entry widget. The number is positive to scroll left to right, 
        # negative to scroll right to left. For example, for an entry widget e, e.xview_scroll(-1, tk.PAGES) would move 
        # the text one “page” to the right, and e.xview_scroll(4, tk.UNITS) would move the text four characters to the left.
        self.entry.xview_scroll(10, tk.UNITS)
    
    def run(self):
        # run master mainloop
        self.master.mainloop()

    def _isOk(self, original_content, changing_content):
        if changing_content == ' ':
            # end changing when input blank
            return False
        else:
            # changing
            cf_logger.info("The entry content is: {}".format(original_content))
            return True

    def _isNotOk(self, original_content):
        cf_logger.info("The end entry content is: {}".format(original_content))


class ScrollbarCustom(object):
    def __init__(self, master=None):
        # get master
        self.master = master

        if self.master is None:
            self.master = tk.Tk()

        # init scrollbar
        self.scrollbar = tk.Scrollbar(self.master)
        self.scrollbar.pack()
    
    def resource_set(self, key, value):
        """
        Set the widget
        :param key: resource name
        :param value: value
        :return: None
        """
        # Setting method:
        # config -> self.scrollbar.config(bg='red'), self.scrollbar.config(background='red'),
        # configure -> self.scrollbar.configure(bg='red'), self.scrollbar.configure(background='red'),
        # self.scrollbar['bg'] = 'red', self.scrollbar['background'] = 'red'

        # Valid resource names: 
        # activebackground, activerelief,
        # background, bd, bg, borderwidth, command, cursor,
        # elementborderwidth, highlightbackground,
        # highlightcolor, highlightthickness, jump, orient,
        # relief, repeatdelay, repeatinterval, takefocus,
        # troughcolor, width.

        self.scrollbar[key] = value

    def resource_set_test(self):
        """
        Set the widget resource name with different value
        
        Only try width, orient, troughcolor, elementborderwidth, repeatdelay, repeatinterval, jump, command
        
        Others refer to FrameCustom, EntryCustom or other above class
        """
        # resource name: width
        # note: Width of the scrollbar (its y dimension if horizontal, and its x dimension if vertical). Default is 16. 
        # resource value: int
        self.resource_set('width', 20)
        cf_logger.info('ScrollbarCustom object set width.')

        # resource name: orient
        # note: Set orient=tk.HORIZONTAL for a horizontal scrollbar, orient=tk.VERTICAL for a vertical one (the default orientation).
        # resource value: int
        self.resource_set('orient', tk.HORIZONTAL)
        cf_logger.info('ScrollbarCustom object set orient.')

        # resource name: troughcolor
        # note: the color of throuth
        # resource value: red, yellow, blue, green ...
        # resource value: #FFFAFA, details in https://www.sioe.cn/yingyong/yanse-rgb-16/
        self.resource_set('troughcolor', 'green')
        self.resource_set('troughcolor', '#FFFAFF')
        cf_logger.info('ScrollbarCustom object set troughcolor.')

        # resource name: elementborderwidth
        # note: The width of the borders around the arrowheads and slider. The default is elementborderwidth=-1,
        # which means to use the value of the borderwidth option.
        # resource value: int
        self.resource_set('elementborderwidth', 10)
        cf_logger.info('ScrollbarCustom object set elementborderwidth.')

        # resource name: repeatdelay
        # note: This option controls how long button 1 has to be held down in the trough before the slider starts moving 
        # in that direction repeatedly. Default is repeatdelay=300, and the units are milliseconds.
        # resource value: int
        self.resource_set('repeatdelay', 500)
        cf_logger.info('ScrollbarCustom object set repeatdelay.')

        # resource name: repeatinterval
        # note: This option controls how often slider movement will repeat when button 1 is held down in the trough. 
        # Default is repeatinterval=100, and the units are milliseconds.
        # resource value: int
        self.resource_set('repeatinterval', 200)
        cf_logger.info('ScrollbarCustom object set repeatinterval.')

        # resource name: jump
        # note: This option controls what happens when a user drags the slider. Normally (jump=0), every small drag of 
        # the slider causes the command callback to be called. If you set this option to 1, the callback isn't called 
        # until the user releases the mouse button.
        # resource value: 0, 1
        self.resource_set('jump', 1)
        cf_logger.info('ScrollbarCustom object set jump.')

        # resource name: command
        # note: A procedure to be called whenever the scrollbar is moved. For a discussion of the calling sequence
        # https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/scrollbar-callback.html 
        # resource value: refer to above link
        entry = tk.Entry(self.master)
        entry.pack(fill='both')

        # entry object set xscrollcommand -> callback function set to scrollbar method .set
        # scrollbar object set commadn -> callback function set to custom method -> 
        # handle entry content in scroll custom callback method
        entry['xscrollcommand'] = self.scrollbar.set
        self.resource_set('command', self._scrollbar_callback)

        # run mainloop
        self.run()

    def run(self):
        # run master mainloop
        self.master.mainloop()

    def _scrollbar_callback(self, *args):
        # just for example, get args, not operate the entry contents
        if args[0] == tk.MOVETO:
            cf_logger.info('scroll callback {} {}'.format(args[0], args[1]))
        elif args[0] == tk.SCROLL:
            cf_logger.info('scroll callback {} {} {}'.format(args[0], args[1], args[2]))

    def methods_scrollbar_test(self):
        """
        Try all common methods of Scrollbar object.
        Not really run below methods, just try to understand it by doc.
        """
        # If no argument is provided, this method returns one of the strings 'arrow1', 'arrow2', 'slider', or '', 
        # depending on where the mouse is. For example, the method returns 'slider' if the mouse is on the slider. 
        # the empty string is returned if the mouse is not currently on any of these three controls.
        # To highlight one of the controls (using its activerelief relief style and its activebackground color), 
        # call this method and pass a string identifying the control you want to highlight, one of 'arrow1', 'arrow2', 
        # or 'slider'.
        self.scrollbar.activate('arrow1') # 'arrow1', 'arrow2', 'slider', or ''

        # Given a mouse movement of (dx, dy) in pixels, this method returns the float value that should be added to the current 
        # slider position to achieve that same movement. The value must be in the closed interval [-1.0, 1.0].
        delta_var = self.scrollbar.delta(1, 2)
        cf_logger.info('delta_var: {}'.format(delta_var))

        # Given a pixel location (x, y), this method returns the corresponding normalized slider position in the interval [0.0, 1.0] 
        # that is closest to that location.
        fraction_var = self.scrollbar.fraction(2, 2)
        cf_logger.info('fraction_var: {}'.format(fraction_var))

        # This method returns a string indicating which (if any) of the components of the scrollbar are under the given (x, y) 
        # coordinates. The return value is one of 'arrow1', 'trough1', 'slider', 'trough2', 'arrow2', or the empty string '' if 
        # that location is not on any of the scrollbar components.
        component_str = self.scrollbar.identify(1, 2)
        cf_logger.info('identify component_str: {}'.format(component_str))

        # Returns two numbers (a, b) describing the current position of the slider. The a value gives the position of the left 
        # or top edge of the slider, for horizontal and vertical scrollbars respectively; the b value gives the position of the
        # right or bottom edge. Each value is in the interval [0.0, 1.0] where 0.0 is the leftmost or top position and 1.0 is
        # the rightmost or bottom position. For example, if the slider extends from halfway to three-quarters of the way along 
        # the trough, you might get back the tuple (0.5,0.75).
        whole_fraction_var = self.scrollbar.get()
        cf_logger.info('whole_fraction_var: {}'.format(whole_fraction_var))

        # To connect a scrollbar to another widget w, set w's xscrollcommand or yscrollcommand to the scrollbar's .set method. 
        # The arguments have the same meaning as the values returned by the .get() method. Please note that moving the scrollbar's 
        # slider does not move the corresponding widget.
        self.scrollbar.set(0.5, 0.75) #command use with other widgets, refer to EntryCustom method test function

class ButtonCustom(object):
    def __init__(self, master=None):
        # get master
        self.master = master

        if self.master is None:
            self.master = tk.Tk()

        # init button
        self.btn = tk.Button(self.master)
        self.btn.config(width=20, height=1, text='ButtonCustom object')
        self.btn.pack()
    
    def resource_set(self, key, value):
        """
        Set the widget
        :param key: resource name
        :param value: value
        :return: None
        """
        # Setting method:
        # config -> self.btn.config(bg='red'), self.btn.config(background='red'),
        # configure -> self.btn.configure(bg='red'), self.btn.configure(background='red'),
        # self.btn['bg'] = 'red', self.btn['background'] = 'red'

        #  STANDARD OPTIONS

        #    activebackground, activeforeground, anchor,
        #    background, bitmap, borderwidth, cursor,
        #    disabledforeground, font, foreground
        #    highlightbackground, highlightcolor,
        #    highlightthickness, image, justify,
        #    padx, pady, relief, repeatdelay,
        #    repeatinterval, takefocus, text,
        #    textvariable, underline, wraplength

        #  WIDGET-SPECIFIC OPTIONS

        #   command, compound, default, height,
        #   overrelief, state, width

        self.btn[key] = value

    def resource_set_test(self):
        """
        Set the widget resource name with different value
        
        Only try width, height, font, state, default, overrelief, repeatdelay, repeatinterval, compound, command
        
        Others refer to FrameCustom, EntryCustom or other above class
        """
        # resource name: width
        # note: Width of the button in letters (if displaying text) or pixels (if displaying an image).
        # resource value: int
        self.resource_set('width', 21)
        cf_logger.info('ButtonCustom object set width.')

        # resource name: height
        # note: Height of the button in text lines (for textual buttons) or pixels (for images).
        # resource value: int
        self.resource_set('height', 1)
        cf_logger.info('ButtonCustom object set height.')

        # resource name: font
        # note: Text font to be used for the button's label - https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/fonts.html
        # resource value: font object
        from tkinter.font import Font

        helv36 = Font(family='Helvetica', size=20, weight='bold')
        self.resource_set('font', helv36)
        cf_logger.info('ButtonCustom object set font.')

        # resource name: state
        # note: state of button
        # https://www.tutorialspoint.com/python3/tk_button.htm
        # resource value: NORMAL='normal', DISABLED='disabled', ACTIVE='active'
        self.resource_set('state', 'normal')
        cf_logger.info('ButtonCustom object set state.')

        # resource name: default
        # note: set default value
        # resource value: NORMAL='normal', DISABLED='disabled', ACTIVE='active'
        self.resource_set('default', 'normal')
        cf_logger.info('ButtonCustom object set default.')

        # resource name: overrelief
        # note: relief value when the mouse over button
        # resource value: RAISED='raised', SUNKEN='sunken', FLAT='flat', RIDGE='ridge', GROOVE='groove', SOLID = 'solid'
        self.resource_set('overrelief', 'groove')
        cf_logger.info('ButtonCustom object set overrelief.')

        # resource name: repeatdelay, repeatinterval,
        # note: Normally, a button fires only once when the user releases the mouse button. If you want the button to fire at 
        # regular intervals as long as the mouse button is held down, set this option to a number of milliseconds to be used 
        # between repeats, and set the repeatdelay to the number of milliseconds to wait before starting to repeat. For example, 
        # if you specify “repeatdelay=500, repeatinterval=100” the button will fire after half a second, and every tenth of a 
        # second thereafter, until the user releases the mouse button. If the user does not hold the mouse button down at least 
        # repeatdelay milliseconds, the button will fire normally.
        # resource value: int
        self.resource_set('“repeatdelay', 500)
        self.resource_set('repeatinterval', 100)
        cf_logger.info('ButtonCustom object set “repeatdelay.')
        cf_logger.info('ButtonCustom object set repeatinterval.')

        # resource name: compound
        # note: If you provide both image and text options, the compound option specifies the position of the image relative to 
        # the text. The value may be tk.TOP (image above text), tk.BOTTOM (image below text), tk.LEFT (image to the left of the text), 
        # or tk.RIGHT (image to the right of the text).When you provide both image and text options but don't specify a compound option, 
        # the image will appear and the text will not.
        # resource value: tk.BOTTOM = 'bottom', tk.CENTER = 'center', tk.LEFT = 'left', None, tk.RIGHT = 'right', or tk.TOP = 'top'
        self.resource_set('compound', 'bottom')
        cf_logger.info('ButtonCustom object set compound.')

        # resource name: command
        # note: callback method or function of the button click
        # resource value: method or function
        self.resource_set('command', self.callback_btn_click)
        cf_logger.info('ButtonCustom object set default.')
        
        # run mainloop
        self.run()

    def callback_btn_click(self):
        cf_logger.info('btn click.')

    def run(self):
        # run master mainloop
        self.master.mainloop()

    def methods_button_test(self):
        """
        Try all common methods of Button object.
        Not really run below methods, just try to understand it by doc.
        """
        # Causes the button to flash several times between active and normal colors. Leaves the button in the state it was
        # in originally. Ignored if the button is disabled.
        self.btn.flash()

        # Calls the button's command callback, and returns what that function returns. Has no effect if the button is disabled 
        # or there is no callback.
        self.btn.invoke()

 
class CheckbuttonCustom(object):
    def __init__(self, master=None):
        # get master
        self.master = master

        if self.master is None:
            self.master = tk.Tk()

        # init button
        self.checkbutton = tk.Checkbutton(self.master)
        self.checkbutton.config(width=20, height=1, text='CheckbuttonCustom object')
        self.checkbutton.pack()
    
    def resource_set(self, key, value):
        """
        Set the widget
        :param key: resource name
        :param value: value
        :return: None
        """
        # Setting method:
        # config -> self.checkbutton.config(bg='red'), self.checkbutton.config(background='red'),
        # configure -> self.checkbutton.configure(bg='red'), self.checkbutton.configure(background='red'),
        # self.checkbutton['bg'] = 'red', self.checkbutton['background'] = 'red'

        # Valid resource names: 
        # activebackground, activeforeground, anchor, overrelief
        # background, bd, bg, bitmap, borderwidth, command, cursor,
        # disabledforeground, fg, font, foreground, height, offrelief
        # highlightbackground, highlightcolor, highlightthickness, image,
        # indicatoron, justify, offvalue, onvalue, padx, pady, relief,
        # selectcolor, selectimage, state, takefocus, text, textvariable,
        # underline, variable, width, wraplength.

        self.checkbutton[key] = value

    def resource_set_test(self):
        """
        Set the widget resource name with different value
        
        Only try indicatoron, variable, offvalue, onvalue, command
        
        Others refer to FrameCustom, ButtonCustom or other above class
        """
        # resource name: offvalue, onvalue
        # note: on and off value of checkbutton
        # resource value: int, float, string
        self.resource_set('offvalue', 10)
        self.resource_set('onvalue', 5)
        cf_logger.info('CheckbuttonCustom object set offvalue.')  
        cf_logger.info('CheckbuttonCustom object set onvalue.')  

        # resource name: indicatoron
        # note: show or high the indicator
        # resource value: 0 or 1
        self.resource_set('indicatoron', 1)
        cf_logger.info('CheckbuttonCustom object set indicatoron.')

        # resource name: variable
        # note: get and set the checkbutton state, like set the check state as 1 or 0, it depends on what the vaule is,
        # refer to offvalue and on value
        # resource value: IntVar, StringVar, DoubleVar object
        ivar = tk.IntVar()
        self.resource_set('variable', ivar)
        # use ivar.get(), ivar.set(1) to get and set the checkbutton state
        cf_logger.info('CheckbuttonCustom object set variable.')  

        # resource name: command
        # note: callback method when checkbutton state changed
        # resource value: methon or function name
        self.resource_set('command', self.callback_checkbutton_click)
        cf_logger.info('CheckbuttonCustom object set command.')  

        # run mainloop
        self.run()

    def callback_checkbutton_click(self):
        cf_logger.info('checkbutton click.')

    def run(self):
        # run master mainloop
        self.master.mainloop()

    def methods_checkbutton_test(self):
        """
        Try all common methods of Button object.
        Not really run below methods, just try to understand it by doc.
        """
        # put the button in off-state
        self.checkbutton.deselect()

        # put the button in on-state
        self.checkbutton.select()

        # toggle the button, on -> off, off -> on
        self.checkbutton.toggle()

        # Toggle the button and invoke the callback method (value of command opption)
        self.checkbutton.invoke()

        # Flashes the checkbutton a few times between its active and normal colors, 
        # but leaves it the way it started.
        self.checkbutton.flash()


class RadiobuttonCustom(object):
    def __init__(self, master=None):
        # get master
        self.master = master

        if self.master is None:
            self.master = tk.Tk()

        # init button
        self.radiobutton1 = tk.Radiobutton(self.master)
        self.radiobutton1.config(width=20, height=1, text='RadiobuttonCustom object1', value=1)
        self.radiobutton1.pack()

        self.radiobutton2 = tk.Radiobutton(self.master)
        self.radiobutton2.config(width=20, height=1, text='RadiobuttonCustom object2', value=2)
        self.radiobutton2.pack()

        self.radiobutton3 = tk.Radiobutton(self.master)
        self.radiobutton3.config(width=20, height=1, text='RadiobuttonCustom object3', value=3)
        self.radiobutton3.pack()
    
    def resource_set(self, key, value):
        """
        Set the widget
        :param key: resource name
        :param value: value
        :return: None
        """
        # Setting method:
        # config -> self.radiobutton.config(bg='red'), self.radiobutton.config(background='red'),
        # configure -> self.radiobutton.configure(bg='red'), self.radiobutton.configure(background='red'),
        # self.radiobutton['bg'] = 'red', self.radiobutton['background'] = 'red'

        # Valid resource names: 
        # activebackground, activeforeground, anchor,
        # background, bd, bg, bitmap, borderwidth, command, cursor,
        # disabledforeground, fg, font, foreground, height,
        # highlightbackground, highlightcolor, highlightthickness, image,
        # indicatoron, justify, padx, pady, relief, selectcolor, selectimage,
        # state, takefocus, text, textvariable, underline, value, variable,
        # width, wraplength.

        self.radiobutton1[key] = value

    def resource_set_test(self):
        """
        Set the widget resource name with different value
        
        Only try value, variable，cammand
        
        Others refer to FrameCustom, ButtonCustom, or other above class
        """
        # resource name: value
        # note: the value of the radio button
        # resource value: int, float, string
        self.radiobutton1['value'] = 1
        self.radiobutton2['value'] = 2
        self.radiobutton3['value'] = 3
        cf_logger.info('CheckbuttonCustom object set value.')  

        # resource name: variable
        # note: get value when click different button
        # resource value: IntVar, DoubleVar, StringVar object
        self.ivar = tk.IntVar()
        self.radiobutton1['variable'] = self.ivar
        self.radiobutton2['variable'] = self.ivar
        self.radiobutton3['variable'] = self.ivar

        # resource name: command
        # note: callback function or method when radio buttion clicked
        # resource value: method name
        self.radiobutton1['command'] = self.callback_radiobutton_click
        self.radiobutton2['command'] = self.callback_radiobutton_click
        self.radiobutton3['command'] = self.callback_radiobutton_click

        # run mainloop
        self.run()

    def callback_radiobutton_click(self):
        tmp_value = self.ivar.get()
        if tmp_value == 1:
            cf_logger.info('radio button 1 click.')
        elif tmp_value == 2:
            cf_logger.info('radio button 2 click.')
        elif tmp_value == 3:
            cf_logger.info('radio button 3 click.')

    def run(self):
        # run master mainloop
        self.master.mainloop()

    def methods_radiobutton_test(self):
        """
        Try all common methods of Button object.
        Not really run below methods, just try to understand it by doc.
        """
        # put the button in off-state
        self.radiobutton1.deselect()
        
        # put the button in on-state
        self.radiobutton1.select()

        # Toggle the button and invoke the callback method (value of command opption)
        self.radiobutton1.invoke()

        # lashes the radiobutton a few times between its active and normal colors, but 
        # leaves it the way it started.
        self.radiobutton1.flash()


class ScaleCustom(object):
    def __init__(self, master=None):
        # get master
        self.master = master

        if self.master is None:
            self.master = tk.Tk()

        # init scale
        self.scale = tk.Scale(self.master)
        self.scale.pack()
    
    def resource_set(self, key, value):
        """
        Set the widget
        :param key: resource name
        :param value: value
        :return: None
        """
        # Setting method:
        # config -> self.scale.config(bg='red'), self.scale.config(background='red'),
        # configure -> self.scale.configure(bg='red'), self.scale.configure(background='red'),
        # self.scale['bg'] = 'red', self.scale['background'] = 'red'

        # Valid resource names: 
        # activebackground, background, bigincrement, bd,
        # bg, borderwidth, command, cursor, digits, fg, font, foreground, from,
        # highlightbackground, highlightcolor, highlightthickness, label,
        # length, orient, relief, repeatdelay, repeatinterval, resolution,
        # showvalue, sliderlength, sliderrelief, state, takefocus,
        # tickinterval, to, troughcolor, variable, width.

        self.scale[key] = value

    def resource_set_test(self):
        """
        Set the widget resource name with different value
        
        Only try bigincrement, digits, from, to, resolution, tickinterval, label, orient, showvalue, 
        sliderlength, sliderrelief, variable, command

        Others refer to FrameCustom, ButtonCustom, or other above class
        """
        # resource name: bigincrement
        # note: Set "big" increases, this option sets the size of the increase, 
        # the default value is 0 and the increment is 1/10 of the range.
        # resource value: int 
        self.scale['bigincrement'] = 0
        cf_logger.info('ScaleCustom object set bigincrement.')

        # resource name: digits
        # note:  Sets the maximum number of digits to display
        # For example, if the from option is 0, the to option is 20, and the digits option is 5, then the 
        # slider is in the range 0.000 to 20.000. Default value is 0 (disabled)
        # resource value: int 
        self.scale['digits'] = 4
        cf_logger.info('ScaleCustom object set digits.')

        # resource name: from, to, resolution，tickinterval
        # note: 2 ends of the scale range, resolution is the step
        # resource value: int 
        self.scale['from'] = 10
        self.scale['to'] = 30
        self.scale['resolution'] = 5
        self.scale['tickinterval'] = 5
        cf_logger.info('ScaleCustom object set from.')
        cf_logger.info('ScaleCustom object set to.')
        cf_logger.info('ScaleCustom object set resolution.')
        cf_logger.info('ScaleCustom object set tickinterval.')

        # resource name: label
        # note: You can display a label within the scale widget by setting this option to the label's text. 
        # The label appears in the top left corner if the scale is horizontal, or the top right corner if 
        # vertical. The default is no label.
        # resource value: string 
        self.scale['label'] = 'Scale title'
        cf_logger.info('ScaleCustom object set label.')

        # resource name: orient
        # note: Set orient=tk.HORIZONTAL if you want the scale to run along the x dimension, or 
        # orient=tk.VERTICAL to run parallel to the y-axis. Default is vertical.
        # resource value: tk.HORIZONTAL, tk.VERTICAL
        self.scale['orient'] = tk.HORIZONTAL
        cf_logger.info('ScaleCustom object set orient.')

        # resource name: showvalue
        # note: Set whether to display the number next to the slider. The default value is True
        # resource value: True or False
        self.scale['showvalue'] = True
        cf_logger.info('ScaleCustom object set showvalue.')

        # resource name: sliderlength, sliderrelief,
        # note: length and relief of slider, slider is the moving part inside the scale; length defult is 30 pixels 
        # resource value: int and "flat"，"sunken"，"groove" 和 "ridge"
        self.scale['sliderlength'] = 50
        self.scale['sliderrelief'] = "sunken"
        cf_logger.info('ScaleCustom object set sliderlength.')
        cf_logger.info('ScaleCustom object set sliderrelief.')

        # resource name: variable
        # note:The control variable for this scale, if any
        # resource value: IntVar, DoubleVar, StringVar object
        self.ivar = tk.IntVar()
        self.scale['variable'] = self.ivar # ivar.get(), ivar.set() to control the scale value
        cf_logger.info('ScaleCustom object set variable.')

        # resource name: command
        # note: callback method or function of scale object, and the latest value will be passed in.
        # resource value: method or function
        self.scale['command'] = self.callback_scale_moving
        cf_logger.info('ScaleCustom object set command.')

        # run mainloop
        self.run()

    def callback_scale_moving(self, value):
        cf_logger.info('scale_moving value:{}'.format(value))

    def run(self):
        # run master mainloop
        self.master.mainloop()

    def methods_scale_test(self):
        """
        Try all common methods of Scale object.
        Not really run below methods, just try to understand it by doc.
        """
        # get the current value as integer or float
        value = self.scale.get()
        cf_logger.info('get value:{}'.format(value))

        # set the value to VALUE
        self.scale.set(30)

        # Returns the coordinates, relative to the upper left corner of the widget, corresponding to
        # a given value of the scale. For value=None, you get the coordinates of the center of the slider 
        # at its current position. To find where the slider would be if the scale's value were set to 
        # some value x, use value=x.
        coords_value = self.scale.coords(40)
        cf_logger.info('coords value:{}'.format(coords_value))

        # Given a pair of coordinates (x, y) relative to the top left corner of the widget, this method 
        # returns a string identifying what functional part of the widget is at that location. 

        # The return value may be any of these:
        # 'slider'	:The slider.
        # 'trough1'	:For horizontal scales, to the left of the slider; for vertical scales, above the slider.
        # 'trough2'	:For horizontal scales, to the right of the slider; for vertical scales, below the slider.
        # ''        :Position (x, y) is not on any of the above parts.

        identify_value = self.scale.identify(0, 2)
        cf_logger.info('identify_value:{}'.format(identify_value))


if __name__ == '__main__':
    # go to test.py
    pass
